# -*- coding: utf-8 -*-
"""
UTc! module: src.lobby.client

Purpose
=======
 Provides routines for synchronously and asynchronously communicating with a
 UTc! lobby server.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv2, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import threading
import socket
import select
import time

_PACKET_SIZE = 8192 #: The maximum amount of data to try to read from the server.
_TIMEOUT = 10.0 #: The number of seconds to wait before considering the connection broken.

class Socket(threading.Thread):
	"""
	A class for managing connections to a UTc! lobby server.
	"""
	_alive = True #: False when this thread should die.
	_data_box = None #: The container that will store read data.
	_lock = None #: A lock used to prevent queue access violations.
	_process_callback = None #: A function used to process lines received from the server.
	_queue = None #: The list of messages waiting to be sent.
	_server = None #: The socket that will be used to communicate with the server.
	
	def __init__(self, host, port, data_box):
		"""
		Initialises the variables required to maintain this connection.
		
		@type host: basestring
		@param host: The address of the host to which a connection will be
		    established.
		@type port: int
		@param post: The port on which the host is running a UTc! lobby server.
		@type data_box: L{src.networking.DataBox}
		@param data_box: The DataBox into which all read data will be placed.
		"""
		threading.Thread.__init__(self)
		self.setDaemon(False)
		
		self._queue = []
		self._lock = threading.Lock()
		
		self._server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self._server.connect((host, port))
		self._data_box = data_box
		
	def forceData(self, data):
		"""
		Sends data to the server.
		
		@type data: str
		@param data: The line of data to be sent, not newline-terminated.
		
		@raise L{ConnectionError}: If the connection to the server was
		    interrupted.
		"""
		try:
			self._server.sendall(data + "\n")
		except:
			raise ConnectionError("Server disconnected")
			
	def kill(self):
		"""
		Indicates that this thread should terminate gracefully.
		"""
		self._alive = False
		
	def readData(self, block=False):
		"""
		Reads data from the connection, storing anything received in
		C{self._data_box}.
		
		@type block: bool
		@param block: If True, this function will raise a TimeoutError if
		    C{_TIMEOUT} seconds pass without any data being received. If False,
		    then this function returns immediately, regardless of whether data
		    was received or not.
		
		@raise L{ConnectionError}: If the connection to the server was
		    interrupted.
		@raise L{TimeoutError}: If no data was received.
		"""
		timeout_counter = 0.0
		
		server = self._server
		data_box = self._data_box
		while self._alive:
			(input_events, output_events, error_events) = select.select([server], [], [server], 0.01)
			if server in input_events:
				try:
					data = server.recv(_PACKET_SIZE)
					if len(data) == 0:
						raise ConnectionError("Server disconnected")
					data_box.feedData(data)
					if data_box.hasLines():
						return
				except:
					raise ConnectionError("Server disconnected")
			elif server in error_events:
				raise ConnectionError("Server disconnected")
			else:
				if not block or data_box.hasLines():
					return
				if timeout_counter >= _TIMEOUT:
					raise TimeoutError("Timed out while waiting for data")
				timeout_counter += 0.05
				time.sleep(0.05)
				
	def run(self):
		"""
		Continually polls for data from the server, using the assigned callback
		function to process any lines that have been received. Also sends any
		queued data to the server each cycle.
		"""
		lock = self._lock
		server = self._server
		queue = self._queue
		data_box = self._data_box
		readData = self.readData
		process_callback = self._process_callback
		
		try:
			while self._alive:
				readData()
				while data_box.hasLines():
					process_callback(data_box.getLine())
					
				lock.acquire()
				if queue:
					server.sendall('\n'.join(queue) + '\n')
					self._queue = queue = []
				lock.release()
				
				time.sleep(0.25)
		except Exception, e:
			process_callback("DISC %s" % (e))
			
		try:
			server.shutdown(socket.SHUT_RDWR)
		except:
			pass
		try:
			server.close()
		except:
			pass
			
	def sendData(self, data):
		"""
		Queues data to be sent to the server.
		
		@type data: str
		@param data: The line of data to be sent, not newline-terminated.
		"""
		self._lock.acquire()
		self._queue.append(data)
		self._lock.release()
		
	def setCallback(self, callback):
		"""
		Assigns a function used to process any lines received from the server.
		
		@type callback: callable
		@param callback: The function to use when handling any lines received from
		    the server.
		"""
		self._process_callback = callback
		
			
class Error(Exception):
	"""
	The base exception from which all exceptions native to this module inherit.
	"""
	def __init__(self, message):
		"""
		Sets the message for a generic exception.
		"""
		Exception.__init__(self, message)
		
class ConnectionError(Error):
	"""
	Indicates that a problem occurred while communicating with the server.
	"""
	pass
	
class TimeoutError(Error):
	"""
	Indicates that the server failed to provide data within the expected
	timeframe.
	"""
	pass
	